okraPDF PDF MCP server
Measured conclusion: Deep-tested. L2 outcome: tools_ok.
This MCP server enables AI agents to view PDFs as accessible HTML with bounding-box citations, and provides tools for layout-aware parsing, schema extraction, cross-document Q&A, and PDF rendering.
| Author | okracomputer |
|---|---|
| Access | remote |
| Publisher key required | No |
| Measured state | Deep-tested |
| L1 liveness | alive |
| L1 latency | 668 ms |
| Last measured | 2026-10-25 |
Add to your agent
Copy this into your MCP client config:
{
"mcpServers": {
"okraPDF PDF MCP server": {
"url": "https://okrapdf.com/mcp",
"transport": "streamable-http"
}
}
}
Measured tools (32)
view_htmlRequires an okraPDF API key (header `Authorization: Bearer okra_...`). Drop a PDF (by public URL) and get back a LIVE, streaming, screen-reader-friendly HTML twin of the document — the same accessible rendering okrapdf.com serves. Returns a viewer_url that progressively streams semantic HTML (headings, lists, tables, figure alt text) as the page is parsed, plus an SSE events_url and a static download_url. Use this to make any PDF readable by assistive tech. (Reserved: policy="wcag" will add the WCAG 2.2 AA / PDF-UA / Section 508 conformance audit — not yet implemented.)
Parameters:
pdf_url(required) — stringtitle— stringpolicy— string
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
verify_sourceVerify a source for a PDF-backed claim and show the result as an inline citation state card. Default mode is high-level: pass document_id, claim, and any rough locator/pages/quote/number; Okra searches parsed nodes, handles not-found and multiple-candidate states, and when a confident source is found mints a durable link.okrapdf.com proof card with quote, page image, bbox focus, confidence, and reasons. For lawfully-reachable PDFs (permitted source or a doc the user has rights to; not paywalled/license-gated) that should not go through full upload/parse yet, pass pdf_url plus page/pages and the tool uses eager_url mode to fetch only that PDF page. verification_requirement defines what counts as verified and defaults to "bbox": eager_url must resolve coordinate evidence before proof is minted, and the API fails rather than returning a proof without bbox. Pass verification_requirement="text_match" only when a deterministic page-text/text-layer match is enough; that returns a found state without a durable proof link, page-image proof, or bbox-backed confidence claim. Use mode="exact_node" with nodes only when the agent is already grounded in exact query rows or node ids. This is the one model-facing source-verification tool; the result also includes create_view arguments for arranging returned card(s) in a custom view.
Parameters:
url— string — Alias for pdf_url.mode— string — Defaults to high-level auto verification. Use exact_node only when passing query rows or node ids; eager_url is inferred when pdf_url is passed without document_id.page— One-based page number for eager URL verification.claim— string — Single-source assertion to verify. Use sources[] for multiple citations.label— string — Optional source label shown on the proof card.nodes— Exact query row(s) or node id(s), only for mode="exact_node" or when the agent is already grounded.pages— string — Rough pages/ranges for a single citation, e.g. "2", "10-12", or "3,7".quote— string — Optional direct quote, number, phrase, or printed value to find and cite.locator— string — Natural-language source hint for a single citation.pdf_url— string — Lawfully-reachable HTTPS PDF URL (permitted source or a doc you have rights to; not paywalled/license-gated) for eager one-page source verification without upload/parse. Provide pages/page too.sources— array — Batch mode: verify several claims and render them as citation cards.document_id— string — okraPDF document ID. Required unless every item in sources has document_id.min_quality— string — Requested citation grade. Defaults to high, but low-confidence/not-found/multiple-candidate states are rendered as cards rather than hidden.rough_pages— string — Alias for pages when the user says rough pages.continue_on_error— boolean — When sources[] is used, keep verified citations even if one item cannot be matched.verification_requirement— string — What the caller considers verified. Defaults to bbox, which requires coordinate evidence before proof is minted. Use text_match only when a deterministic text-layer/page-text hit is sufficient and no proof link is needed.
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
upload_documentUpload a PDF from a lawfully-reachable URL or base64 PDF bytes for Gemini Flash VLM extraction when later okraPDF tool calls need a document_id for SQL/search/docs.read, render_ui, view_document/review_extraction, parsed-node verification, or workflow runs. URL ingest is corpus-gated (see internal/content-rights-policy.md): open/public-record sources or docs the user has rights to — not paywalled/license-gated. For such URLs, prefer resolve_pdf_url so the D1 URL registry can reuse prior ingests. If you only need to read or understand PDF content for reasoning, use normal web_fetch/browser reading first; for arXiv papers, prefer arxiv.org/html/... when available. Do not use this for "verify", "cite", "prove", "source", or "where in the PDF" requests; call verify_source directly with pdf_url + page/pages instead. Opens a live document viewer immediately; the app polls status, page images, and extracted blocks as they arrive. Set wait=true only for legacy blocking status behavior.
Parameters:
url— string — Lawfully-reachable URL of the PDF (open/public-record source or a doc you have the rights to; not behind a paywall/login). License-gated sources are refused.data— string — Base64-encoded PDF bytes. MCP-only fallback when no public URL or normal multipart upload handoff exists.wait— boolean — Legacy blocking mode. Defaults to false so the MCP App opens immediately and polls live document state.file_name— string — Filename for base64 uploads. Defaults to document.pdfprocessor— string — OCR processor/vendor id. Defaults to "gemini-vision", the shared Gemini Flash VLM parser used by /v1/parse. Advanced legacy values include "textlayer", "unstructured", "azure-di", and "parse-proxy".document_id— string — Custom document ID (auto-generated if omitted)page_images— string — Page image rendering strategy. 'none' = no rendering; 'cover' = page 1 only; 'eager' = ALL pages rendered at upload via the PdfRasterizer Container (default). 'lazy' is a deprecated alias for 'eager'.vendor_options— object — Vendor-specific options passed through to the selected parsing vendor (AI SDK providerOptions pattern).
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
resolve_pdf_urlResolve a lawfully-reachable PDF URL to a stable okraPDF document_id using the D1 URL registry when later okraPDF tool calls need that document_id: execute_code SQL/search/docs.read, render_ui, view_document/review_extraction, verify_source with parsed-node evidence, or workflow runs. Intended for open/public-record sources (SEC EDGAR, gov, public-domain, permissively-licensed arXiv) and for documents the user supplies or has the rights to process. Do NOT use it to fetch behind a paywall, login, or anti-bot wall, and do NOT treat the resulting derivative as a public mirror of a copyrighted source — see internal/content-rights-policy.md (mirror levels). If you only need to read or understand the PDF content for reasoning, use normal web_fetch/browser reading first; for arXiv papers, prefer the arxiv.org/html/... page when available because it is cleaner and cheaper than OCR. Do not use this for "verify", "cite", "prove", "source", or "where in the PDF" requests; call verify_source directly with pdf_url + page/pages instead. Opens the live document viewer immediately while ingest/parse/page previews finish. Do not set wait_for/wait_ms unless the very next tool call depends on query/page/visual readiness.
Parameters:
url(required) — string — Lawfully-reachable HTTPS URL for a PDF (open/public-record source or a doc you have rights to; not paywalled/license-gated — those are refused). arXiv /abs and /html URLs normalize to the direct PDF URL.pages— string — Optional page range/list used with wait_for="page" and passed to the live viewer, e.g. "23" or "1-3".wait_ms— integer — Optional readiness wait budget. Defaults to 0 so the resolver returns quickly after starting ingest. Only set this when the very next tool call depends on parse/page/visual readiness.wait_for— string — Optional readiness target for wait_ms. document=phase ready, query=nodes ready, page=requested pages have images, visual=initial viewer preview images ready.processor— string — OCR processor/vendor id for the parse. Defaults to "gemini-vision" (layout-aware Gemini Flash VLM) so view_document gets bbox-bearing citation overlays. Pass "textlayer" for a fast text-only parse without overlays.open_viewer— boolean — Whether to open the live MCP document viewer. Defaults to true; the tool is UI-bound so hosts can show progress immediately.
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
describe_collectionCall this FIRST, before any execute_code or render_ui SQL. Returns collection metadata, the document list, the SQLite schema, the node data model (what a node / table / cell is, that cell values are raw strings and that row/column headers are positional sibling cells, not labels), and example queries — the context you need to write a working query on the first try.
Parameters:
document_id— string — Optional document ID to probe for table serialization shape and document-specific query hints.collection_id— string — Collection ID, slug, or name. Omit to describe all documents accessible to the current API key.
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
execute_codeExecute JavaScript against Code Mode namespaces — the primary tool for exploring, querying, grepping, reading, and citing documents (results come back here). For a finished chart, table, metric, or HTML display, pass the working query to render_ui instead. Code Mode API: docs.list() → [{id, file_name, status, total_pages}] docs.status({ docId }) → {phase, totalPages, totalNodes, ...} docs.read({ docId, pages? }) → {markdown, page_count, total_pages, truncated?} docs.ask({ docId, question }) → {answer, citations, trace_id} (LLM — slow, use sparingly) docs.extract({ docId, prompt, jsonSchema }) → {data, trace_id} (LLM — slow, use sparingly) sql.query({ docId, sql }) → {rows, count} — read-only SQL on one doc; rows include document_id sql.queryMany({ docIds, sql }) → {results:[{docId, rows, count}], count} — run the same query across docs sql.search({ query, docId?, docIds? }) → FTS5 keyword/phrase search across one doc or the current collection sql.grep({ query, docId?, docIds?, pages? }) → literal substring search across node values; useful when FTS tokenization is too strict citation.create({ claim, nodes, docId?, minQuality? }) → {id, url, verification} — mint a shareable link.okrapdf.com/c/{id} proof card; minQuality defaults to "high". citation.batch({ items }) → {citations:[{id,url,...}], count} — mint many proof links from multi-doc results. SQLite tables per doc: nodes (id, parent_id, type, label, value, status, page_number, confidence, metadata, sort_order) nodes_fts — FTS5 on label+value (use MATCH) meta (key, value) page_ledger (page_number, status, pass, vendor, attempt, confidence, error) Node data model — the `nodes` table is the extracted document tree: - Each row is one element. `type` is the raw parser type and varies by vendor, so always run SELECT DISTINCT type FROM nodes first (common values: text, table, row, cell, figure, heading). - Tables come in TWO shapes depending on the parser; check with SELECT DISTINCT type: (a) a single `table` node with the whole table serialized in its `value` (e.g. the gemini-vision default), or (b) a nested `table` → `row` → `cell` tree: row.parent_id = table.id, cell.parent_id = row.id, and each `cell.value` holds one cell's text (table/row values are usually empty). For shape (b) read a grid by joining BOTH levels and ordering by sort_order; row/column headers are the first/edge cells positionally, NOT stored on `label` (which is usually NULL). - `value` is always raw text exactly as printed ("88,268", "16 %", "Sept 30, 2025") — never a typed number/date. Cast in SQL for arithmetic, e.g. CAST(REPLACE(REPLACE(value,',',''),'$','') AS REAL). Prefer SQL/FTS for discovery, filtering, keyword search, counting, and reading content. SQL is fast, free, and deterministic. Only fall back to reading full pages when SQL doesn't have what you need. When answering numerical or factual questions, use sql.query() to find exact values, then compute the answer step by step. If the first query doesn't return what you need, try a different query — iterate rather than guessing from partial data. IMPORTANT: nodes_fts rows contain full page text (5KB+ each). Always use substr() and JOIN nodes for page_number: SELECT n.page_number, substr(n.value, 1, 200) as excerpt FROM nodes_fts f JOIN nodes n ON n.id = f.node_id WHERE nodes_fts MATCH 'keyword' LIMIT 10 High-grade citation workflow: - Never cite from memory or from a generated answer alone. First use SQL/FTS/search to find exact candidate nodes. - For MCP users, prefer verify_source when you have a claim plus rough locator/pages/quote; pass pdf_url + page/pages for eager one-page public-PDF verification without full ingest, or document_id for parsed-node verification. It renders found/not-found/multiple-candidate states as an inline card. - verification_requirement controls what counts as verified. The default is verification_requirement="bbox": require coordinate evidence and only mint a proof link when a bbox is available. Pass verification_requirement="text_match" only when deterministic page-text/text-layer matching is sufficient; it returns a found state without a durable proof link, page-image proof, or bbox-backed confidence claim. - If you already have exact rows, pass rows that include id, document_id, page_number, value, bbox_x/y/w/h, confidence, and status to verify_source mode="exact_node" or citation.create. - For user-facing, audit, legal, or financial claims, visually verify the page/bbox with view_document or review_extraction before presenting the citation as final when the card says multiple_candidates, not_found, or low confidence. - Treat the returned verification_grade, verification_score, and verification_warnings as the citation quality gate; if warnings mention missing bbox, low confidence, or unverified status, disclose that or find better evidence. Only use ask/extract when the answer requires LLM reasoning, not data lookup. Page images: https://api.okrapdf.com/v1/documents/{docId}/pg_{N}.png
Parameters:
code(required) — string — Async JavaScript body to execute against your document library. Runs in a V8 isolate. No network, no npm — but ESM imports work at render time (esm.sh). Code Mode namespaces: library.list() → [{id, file_name, status, total_pages}] — your whole document library (docs.list = deprecated alias) library.search({ name?, status?, minPages?, maxPages?, limit?, offset? }) → {documents,count} — filter the whole LIBRARY by name/metadata (vs sql.search, which searches text INSIDE a doc) docs.status({ docId }) → {phase, totalPages, totalNodes, ...} docs.read({ docId, pages? }) → {markdown, page_count, total_pages, truncated?} docs.ask({ docId, question }) → slower LLM answer over one doc docs.extract({ docId, prompt, jsonSchema }) → structured LLM extraction sql.query({ docId, sql }) → {rows, count}; stamps rows with document_id sql.queryMany({ docIds, sql }) → {results:[{docId, rows, count}], count} sql.search({ query, docId?, docIds? }) → FTS5 keyword/phrase search sql.grep({ query, docId?, docIds?, pages? }) → literal substring search citation.create({ claim, nodes, docId? }) → {id, url, verification} citation.batch({ items }) → {citations:[{id,url,...}], count} Action verbs (orchestrate: parse · tools · publish — on docs you OWN): docs.engines() → {engines:[{id,tier,category}], count} — the parse-engine menu (free) docs.parse({ docId, vendor }) → re-parse with an explicit engine id (pin/switch); waits, returns {phase, totalNodes, completed} docs.publish({ docId }) → publish a finished doc to a public read URL → {publicDocId, url} tools.extract({ docId, prompt, jsonSchema }) → okrapdf /extract (structured data) tools.to_json({ docId, schema }) → okrapdf /pdf-to-json (document → JSON in your schema) // Compare engines: call docs.parse per vendor (a dynamic workflow can parallel() them) — each call re-parses the same doc. SQLite tables per doc: nodes (id, parent_id, type, label, value, status, page_number, confidence, metadata, sort_order) nodes_fts — FTS5 on label+value (use MATCH) meta (key, value) | page_ledger (page_number, status, pass, vendor, attempt, confidence, error) Return values: return object/array → rendered as JSON viewer on embed return "<div>..." → auto-wrapped in HTML shell (dark theme, system-ui) return "<!DOCTYPE…" → served as full HTML document (you control everything) return { html, scripts } → html body + scripts block, auto-wrapped in shell Rendering HTML views: Return an HTML string. Fragments like "<h1>Hello</h1>" are auto-wrapped in a shell. For full control, return a complete document starting with "<!DOCTYPE html>". For interactive views, use ESM imports — the CSP allows esm.sh, jsdelivr, cdnjs: return `<div id="app"></div> <script type="module"> import { Chart } from 'https://esm.sh/chart.js@4'; // Chart.js, React, D3, Plotly, Three.js — any ESM package works </script>`; Page images available at: https://res.okrapdf.com/v1/documents/{docId}/pg_{N}.png Common SQL: Page content: SELECT value FROM nodes WHERE page_number = 5 ORDER BY sort_order FTS search: SELECT n.page_number, substr(n.value, 1, 200) as excerpt FROM nodes_fts f JOIN nodes n ON n.id = f.node_id WHERE nodes_fts MATCH 'revenue' LIMIT 10 Cross-doc: sql.queryMany({ docIds, sql }) IMPORTANT: nodes_fts rows are full page text (5KB+). Always use substr() to avoid huge results.public_doc_ids— array — Optional public doc IDs to mount alongside the authenticated user’s private docs for mixed private/public multi-document analysis.
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
open_pdf_workspaceread-onlyOpen an Okra PDF workspace to extract a page with the current conversation model and review source-aligned bounding boxes. Opening does not start OCR. Supply document_id for an existing Okra document, or open the workspace to choose a PDF.
Parameters:
file— objectpage— integerdocument_id— string
Hints: read-only yes · destructive no · idempotent not declared · open-world no
prepare_page_layoutWhen the user explicitly requests page extraction, supply the source page image and the pinned ParseBench layout prompt to the current model. Read the returned image yourself and call submit_page_layout with the result. This tool performs no model inference or OCR.
Parameters:
document_id(required) — stringpage(required) — integerview_uuid— string
Hints: read-only not declared · destructive no · idempotent not declared · open-world no
submit_page_layoutSubmit your extraction from prepare_page_layout as ParseBench div wrappers with data-bbox=[x1,y1,x2,y2] on the 0–1000 grid and data-label, or normalized proposal JSON. Only proposals are saved; this never verifies content or approves redactions.
Parameters:
document_id(required) — stringrun_id(required) — stringpage(required) — integerproposal— objectlayout_html— string
Hints: read-only not declared · destructive no · idempotent not declared · open-world no
poll_page_layoutread-onlyInternal: refresh this viewer’s conversation layout proposal.
Parameters:
document_id(required) — stringrun_id(required) — stringview_uuid(required)
Hints: read-only yes · destructive no · idempotent not declared · open-world no
import_pdf_for_reviewInternal: import a user-selected PDF without starting OCR or any model provider.
Parameters:
data(required) — stringfile_name(required) — string
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
view_documentShow the VISUAL extraction of a parsed okraPDF document: page images with colored bounding-box overlays over the extracted blocks that have bbox data (a bounded set per page), plus a clickable block list (click a box to highlight its text, and vice versa). Use after upload_document to let the user see and verify the extraction. Pass document_id and optional pages. For the structured text output instead (parsed nodes as plain reading HTML, no images), use inspect_html.
Parameters:
document_id(required) — string — okraPDF document ID to visualize (e.g. doc-abc123).pages— string — Pages to render, 1-indexed. Range ("1-5"), list ("1,3,7"), or single ("2"). Defaults to the first 3 pages. Capped at 8.
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
view_pdfOpen the document as a PDF reader/navigation surface inside the MCP host. Shows page images and, when available, extracted bounding boxes. Use this for quick visual page inspection; use review_extraction when the user is specifically verifying parser output.
Parameters:
document_id(required) — string — okraPDF document ID to visualize (e.g. doc-abc123).pages— string — Pages to render, 1-indexed. Range ("1-5"), list ("1,3,7"), or single ("2"). Defaults to the first 3 pages. Capped at 8.
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
review_extractionOpen the extraction review workflow: page images with bbox overlays and clickable extracted blocks that can be verified or flagged. This is the bbox/provenance workflow surface, not a PDF editor.
Parameters:
document_id(required) — string — okraPDF document ID to visualize (e.g. doc-abc123).pages— string — Pages to render, 1-indexed. Range ("1-5"), list ("1,3,7"), or single ("2"). Defaults to the first 3 pages. Capped at 8.
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
verify_blockMark an extracted block as verified (✓) or wrong (✗), persisted to the document. Called by the view_document viewer when the user clicks a block; also usable directly with document_id + node_id.
Parameters:
document_id(required) — string — okraPDF document ID the block belongs to.node_id(required) — string — Node/block ID to verify (from a view_document overlay).note— string — Optional human note, stored with the verification.approved— boolean — true → mark the block verified (✓). false → flag it as wrong (✗).
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
interactDrive the document viewer the user already has open (from view_document): navigate, highlight a node, or auto-highlight text with the pdf-server-style highlight_text arguments query/page/color/content. Pass document_id plus view_uuid or viewUUID from the view_document result. Use commands or actions for ordered batches. Do NOT call view_document again to navigate — that opens a separate viewer.
Parameters:
document_id(required) — string — okraPDF document ID being viewed.page— integer — 1-indexed page for action="navigate".color— string — Highlight color for action="highlight_text", e.g. "#FFFF00".query— string — Text to find/highlight. For highlight_text, the viewer auto-finds matching text geometry.action— string — Single viewer action. Use commands for batching.actions— array — Alias for commands, matching pdf-server multi-action interact calls.content— string — Tooltip/label content for action="highlight_text".node_id— string — Block/node id to focus for action="highlight" (from a view_document overlay).commands— array — Batch of viewer actions, processed sequentially.viewUUID— string — Alias for view_uuid, matching the pdf-server interact argument shape.view_uuid— string — The open viewer's view_uuid (from the view_document result).match_index— integer — 0-indexed match for action="search_navigate".
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
poll_document_viewInternal: the viewer widget refreshes upload/parse/render status, page images, and extracted blocks. Not for direct agent use.
Parameters:
document_id(required) — string — okraPDF document ID being viewed.view_uuid(required) — string — The open viewer view_uuid from upload_document or view_document.pages— string — Pages to refresh, using the same range/list syntax as view_document. Defaults to the first 3 pages.
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
poll_view_commandsInternal: the viewer widget drains queued interact commands. Not for direct agent use.
Parameters:
document_id(required) — string — okraPDF document ID being viewed.view_uuid(required) — string — The open viewer's view_uuid.
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
inspect_htmlShow the STRUCTURED text output of a parsed document: the canonical extracted nodes rendered as plain webpage-like HTML pages — no page images, no bounding boxes. Omit page to render the available pages together (capped, ~50 pages for long docs); pass page for one specific page. Uses canonical DocumentAgent nodes (not vendor playground facets) and returns status="ready" only when node-backed HTML exists, else a not-ready state. For the visual extraction (page images + bounding-box overlays) instead, use view_document.
Parameters:
document_id(required) — string — okraPDF document ID to inspect.page— integer — Optional 1-indexed page number to inspect. Omit to render every same-document HTML page in one inspector container.facet— string — Deprecated no-op retained for older clients. inspect_html always renders canonical document nodes.selector— string — CSS selector/query to run inside the PDFQuery Inspector UI (e.g. "table", "[data-label=Table]").parse_facet— string — Deprecated no-op retained for older clients. inspect_html always renders canonical document nodes.parse_if_missing— boolean — Deprecated no-op retained for older clients. inspect_html never queues a parser run.
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
view_structuredOpen the parsed structured text/HTML view inside the MCP host. Use this for reading extracted document content as HTML without page images or bbox overlays; use view_pdf/review_extraction for visual source pages.
Parameters:
document_id(required) — string — okraPDF document ID to inspect.page— integer — Optional 1-indexed page number to inspect. Omit to render every same-document HTML page in one inspector container.facet— string — Deprecated no-op retained for older clients. inspect_html always renders canonical document nodes.selector— string — CSS selector/query to run inside the PDFQuery Inspector UI (e.g. "table", "[data-label=Table]").parse_facet— string — Deprecated no-op retained for older clients. inspect_html always renders canonical document nodes.parse_if_missing— boolean — Deprecated no-op retained for older clients. inspect_html never queues a parser run.
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
render_uiRun codemode against docs/sql/citation and render the returned visualization as an MCP App. Use for charts, tables, metrics, and small dashboards derived from PDFs after you know the schema. Call describe_collection first, and validate exploratory SQL with execute_code before embedding it here; SQL failures are returned as codemode errors. The codemode sandbox includes the preinstalled "okra-render-ui" module with querySql() plus prebuilt component helpers like TimeSeriesLineChart(), DataTable(), MetricCards(), dashboard(), and chart/table/html specs; keep query code separate from reusable UI helpers, return compact data, not screenshots, and do not rely on CDN scripts such as Chart.js inside html().
Parameters:
code(required) — string — JavaScript codemode source that queries docs/sql/citation and returns a UI spec for the MCP Apps render_ui widget. Runs in the same isolated V8 codemode environment as execute_code: no network, but docs/sql/citation are available. Before writing SQL, call describe_collection to inspect the document list and schema. For exploratory SQL, validate with execute_code first, then move the working query into render_ui. SQL errors are terminal tool errors, and the renderer only gets the UI payload after codemode succeeds. Preinstalled module: const { querySql, render_ui, dashboard, TimeSeriesLineChart, DataTable, MetricCards, barChart, lineChart, areaChart, scatterPlot, vegaLite, proofCard, table, metric, html } = await import("okra-render-ui.js"); SQLite tables per doc: nodes (id, parent_id, type, label, value, status, page_number, confidence, metadata, sort_order) nodes_fts — FTS5 on label+value (use MATCH) meta (key, value) page_ledger (page_number, status, pass, vendor, attempt, confidence, error) Node data model — the `nodes` table is the extracted document tree: - Each row is one element. `type` is the raw parser type and varies by vendor, so always run SELECT DISTINCT type FROM nodes first (common values: text, table, row, cell, figure, heading). - Tables come in TWO shapes depending on the parser; check with SELECT DISTINCT type: (a) a single `table` node with the whole table serialized in its `value` (e.g. the gemini-vision default), or (b) a nested `table` → `row` → `cell` tree: row.parent_id = table.id, cell.parent_id = row.id, and each `cell.value` holds one cell's text (table/row values are usually empty). For shape (b) read a grid by joining BOTH levels and ordering by sort_order; row/column headers are the first/edge cells positionally, NOT stored on `label` (which is usually NULL). - `value` is always raw text exactly as printed ("88,268", "16 %", "Sept 30, 2025") — never a typed number/date. Cast in SQL for arithmetic, e.g. CAST(REPLACE(REPLACE(value,',',''),'$','') AS REAL). Recommended modular style: keep data access in sql.query()/querySql(), then pass the rows into a prebuilt component helper such as TimeSeriesLineChart(), DataTable(), MetricCards(), dashboard(), barChart(), lineChart(), table(), or metric(). This mirrors product UI components without hardcoding screenshots or CDN chart libraries. Return one of — dashboard()/chart/table/metric helpers render inside Okra's bundled MCP App without external libraries. html() is the flexible escape hatch for fully custom layouts, but external scripts/resources are blocked by the MCP App CSP; do not use CDN libraries such as Chart.js, Plotly, or D3. Use dashboard(), chart helpers, or inline SVG/CSS instead: - html("<h1>...</h1>") or { type: "html", html: "<!doctype html>..." } - TimeSeriesLineChart({ title, labels, series: [{ key, label, data }] }) for PostHog-style time-series data - dashboard({ title, items: [barChart(...), lineChart(...), metric(...), table(...)] }) - barChart({ title, data, x, y, series? }) - lineChart({ title, data, x, y, series? }) - vegaLite({ mark, data: { values }, encoding }) for constrained Vega-Lite-like grouped bars / multi-series lines - proofCard({ claim, page_image, bbox, text, url }) to render citation proof inline - scatterPlot({ title, data, x, y, series? }) - table({ title, rows, columns? }) - metric({ title, metrics: [{ label, value, delta? }] }) - any object/array, which renders as JSON/table fallback Examples: // 1) bar chart of blocks per page async () => { const { querySql, barChart } = await import("okra-render-ui.js"); const [{ id: docId }] = await docs.list(); const rows = await querySql("SELECT page_number, count(*) AS blocks FROM nodes GROUP BY page_number ORDER BY page_number", { docId }); return barChart({ title: "Blocks per page", data: rows, x: "page_number", y: "blocks" }); } // 2) query first, then use a reusable product-style component async () => { const { querySql, TimeSeriesLineChart } = await import("okra-render-ui.js"); const [{ id: docId }] = await docs.list(); const rows = await querySql("SELECT page_number AS day, count(*) AS user_count FROM nodes GROUP BY page_number ORDER BY page_number", { docId }); return TimeSeriesLineChart({ title: "Blocks over pages", labels: rows.map((r) => String(r.day)), series: [{ key: "blocks", label: "Blocks", data: rows.map((r) => Number(r.user_count)) }], }); } // 3) nested table → row → cell grid (only when tables are shape (b); cell.value holds each cell) async () => { const { querySql, DataTable } = await import("okra-render-ui.js"); const [{ id: docId }] = await docs.list(); const rows = await querySql("SELECT r.sort_order AS row, c.sort_order AS col, c.value FROM nodes t JOIN nodes r ON r.parent_id = t.id AND r.type = 'row' JOIN nodes c ON c.parent_id = r.id AND c.type = 'cell' WHERE t.type = 'table' ORDER BY t.id, r.sort_order, c.sort_order LIMIT 100", { docId }); return DataTable({ title: "Table cells", rows }); } // 4) FTS keyword search rendered as custom HTML via html() async () => { const { querySql, html } = await import("okra-render-ui.js"); const [{ id: docId }] = await docs.list(); const rows = await querySql("SELECT n.page_number, substr(n.value,1,200) AS excerpt FROM nodes_fts f JOIN nodes n ON n.id = f.node_id WHERE nodes_fts MATCH 'revenue' LIMIT 10", { docId }); return html("<ul>" + rows.map((r) => "<li>p" + r.page_number + ": " + r.excerpt + "</li>").join("") + "</ul>"); }title— string — Optional fallback title for the rendered UI.height— integer — Preferred inline widget height in pixels.document_id— string — Optional primary okraPDF document ID to mount for this visualization. Use after resolve_pdf_url when the user points at a public PDF.public_doc_ids— array — Optional public document IDs to mount alongside the authenticated user library for multi-document visualizations.
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
create_viewread-onlyRender a model-authored chart, table, metric, proof card, or dashboard as an MCP App and progressively preview safe component JSON while tool arguments stream. Use when you already have the data. For document-backed SQL/codemode work, use render_ui instead. Input view is a compact JSON string; top-level arrays are dashboard items. Do not pass HTML, scripts, iframes, or CDN-dependent payloads.
Parameters:
view(required) — string — Compact JSON string describing a model-authored Okra render view. Use this only when you already have the data and want the MCP App to render progressively while arguments stream. Supported safe component types: dashboard, table, metric, bar, line, area, scatter, vega-lite, proof-card, text, and json. Top-level arrays are treated as dashboard items. Do not use html/script/iframe payloads here; arbitrary HTML belongs in render_ui after server-side codemode completes.title— string — Optional title for the rendered view.height— integer — Preferred inline widget height in pixels.
Hints: read-only yes · destructive not declared · idempotent not declared · open-world not declared
get_render_payloadInternal: the render_ui viewer fetches a large visualization payload that was stashed out-of-band. Not for direct agent use.
Parameters:
render_id(required) — string — render_id from a render_ui result whose visualization payload was stashed out-of-band for the viewer to fetch.
Hints: read-only not declared · destructive not declared · idempotent not declared · open-world not declared
play_music_scoreread-onlyRender and play a PDF music-score extraction from semantic note events with normalized page bboxes. Use this when demonstrating non-text PDF accessibility: each note has page coordinates, pitch, timing, and an accessible label; the UI synchronizes the source bbox overlay with a playable score. If no input is provided, it opens a seeded public-domain Beethoven Moonlight Sonata excerpt from Mutopia. For real OMR output, pass abc_notation plus notes[].
Parameters:
page— integer — Source PDF page number for the note bboxes. Defaults to 1.notes— array — Semantic notes with normalized PDF-page bboxes, timing, pitch, and accessibility labels.title— string — Optional score title. Defaults to the demo excerpt title.composer— string — Optional composer label.tempo_bpm— number — Playback tempo in quarter-note beats per minute. Defaults to 54.abc_notation— string — ABC notation to render as playable sheet music. Defaults to the demo excerpt.source_label— string — Optional source label shown to the model and app.mapping_source— string — Short provenance label for the bbox-to-note mapping, e.g. "audiveris", "manual-review", or "seeded-demo".page_image_url— string — Optional rendered page image URL/data URI for the source PDF page. If omitted, the widget draws a compact generated page from the bboxes.source_pdf_url— string — Optional source score PDF URL. Defaults to the public-domain Mutopia Moonlight Sonata PDF.
Hints: read-only yes · destructive not declared · idempotent not declared · open-world not declared
list_workflowsread-onlyList workflows you authored, newest first, so you can recover workflow_id values from prior MCP sessions before calling view_workflow or run_workflow.
Parameters:
limit— integer — Maximum workflows to return, newest first. Defaults to 20.
Hints: read-only yes · destructive not declared · idempotent not declared · open-world no
list_workflow_runsread-onlyList workflow runs you started, newest first, so you can recover run_id values from prior MCP sessions before calling view_workflow_run.
Parameters:
limit— integer — Maximum runs to return, newest first. Defaults to 20.
Hints: read-only yes · destructive not declared · idempotent not declared · open-world no
view_workflow_runread-onlyMonitor a workflow run. For agent workflow scripts, returns status, stats, controller_output, per-agent outputs, event stream, logs, and failed-agent details. For step-definition workflows (e.g. invoice extraction), returns overall status, per-document rows, exceptions, dropped docs, stats, and the human-approval state. Pass the run_id from POST /v1/runs.
Parameters:
run_id(required) — string — Run id (e.g. dynrun_…) returned by POST /v1/runs or a prior view_workflow_run call.
Hints: read-only yes · destructive not declared · idempotent not declared · open-world no
view_workflowread-onlyConfirm a workflow as it is being built: finite definitions return visualization + validation, while agent workflow scripts return parse/readiness state and a best-effort plan estimate. Accepts a catalog name (e.g. staff.table-ingest) or a workflow id you authored.
Parameters:
workflow_id(required) — string — Workflow id or catalog name to confirm/visualize (e.g. staff.table-ingest, or a wf_… you authored).
Hints: read-only yes · destructive not declared · idempotent not declared · open-world no
approve_runApprove or reject a run that is waiting_for_review (the human-in-the-loop gate). Approving emits the validated rows; rejecting discards the run. Returns the refreshed run snapshot.
Parameters:
run_id(required) — string — Run id of the waiting_for_review run to decide on.decision(required) — string — approved emits the rows; rejected discards the run.note— string — Optional reviewer note recorded on the decision.
Hints: read-only no · destructive no · idempotent no · open-world no
draft_workflowAuthor, create, build, or draft a workflow. For custom/agentic work, pass `code` as a high-level agent workflow script using `agent()` / `parallel()` / `pipeline()` / `phase()`. Minimal working code: `phase("Extract"); const result = await agent("Extract the invoice total.", { label:"extract", schema:{ type:"object", required:["total"], properties:{ total:{ type:"string" } } } }); return { result };`. Each agent call must include a task prompt and a JSON Schema under the `schema` key, e.g. agent("task", { label:"extract", schema:{ type:"object", required:["rows"], properties:{ rows:{ type:"array" } } } }) or agent({ prompt:"task", label:"extract", schema:{...} }). Plain JS acts as the deterministic controller. This is the path for provider A/B tests, N-provider parser fan-out, judge/compare barriers, and human review via an explicit detect/apply phase split. `definition` and `catalog_workflow_id` are only for finite catalog pipelines. Returns workflow_id, readiness, blueprint, and AST when available. Persisted to your account — same as POST /v1/workflows.
Parameters:
run— boolean — Start a run immediately after creating when the selected runtime is configured.code— string — High-level agent workflow script. Author in the agent()/parallel()/pipeline()/phase() grammar. Minimal working script: phase("Extract"); const result = await agent("Extract the invoice total.", { label: "extract", schema: { type: "object", required: ["total"], properties: { total: { type: "string" } } } }); return { result }; Every agent step MUST include a `schema` JSON Schema contract: agent("task prompt", { label: "extract", schema: { type: "object", required: ["rows"], properties: { rows: { type: "array" } } } }) or agent({ prompt: "task prompt", label: "extract", schema: {...} }). The contract key is `schema`; do not use returns/output_schema/json_schema. Barriers are explicit with parallel(() => agent(...)) thunks; pipeline(items, mapper) is sequential by default and can fan out over runtime data with { concurrency }. phase() names the user-visible stage, and human review is modeled as a detect/apply phase split. Use this for provider A/B tests, N-provider parser fan-out, judge/compare barriers, redaction/extraction review, and other custom agent work. The script is captured as the workflow source artifact and runs through the hosted JS controller harness when the runtime bindings are configured.name— string — Human-readable workflow name.inputs— Run inputs, e.g. { files: ["doc-…"] } for invoice extraction or { document_id: "doc_…" } for an agent workflow. Setting this implies run:true.definition— object — Finite catalog pipeline only: { steps: [{ type, id?, name?, with?, needs? }] } using GET /v1/steps types. Use this for staff/catalog pipelines, not custom agent work.catalog_workflow_id— string — Instantiate a staff template as your own workflow (e.g. staff.invoice-extraction, staff.table-ingest).
Hints: read-only no · destructive no · idempotent no · open-world no
run_workflowStart a run of a runnable workflow you authored (the write counterpart to view_workflow_run). Pass the `workflow_id` from draft_workflow and `inputs` (e.g. {files:["doc-…"]} or {document_id:"doc_…"}). Catalog/step-definition invoice pipelines may surface waiting_for_review; agent workflow scripts run through the hosted JS controller harness and return controller_output plus per-agent outputs and event stream when complete. Same as POST /v1/runs.
Parameters:
workflow_id(required) — string — Workflow id (wf_…) to run, as returned by draft_workflow.inputs— Run inputs, e.g. { files: ["doc-…"] } for invoice extraction (the documents to process).
Hints: read-only no · destructive no · idempotent no · open-world no
submit_usage_feedbackSubmit detailed feedback about using the okraPDF MCP server. Use this only when the user asks to report feedback or when a reproducible MCP tool/app issue, confusing workflow, missing affordance, or useful agent-DX note should be sent to the okraPDF team. Pass the long feedback string in `feedback`; include workflow_id or wf_id, run_id, document_id, tool_name, client_name, tags, and other JSON-serializable debug context in `options`. Do not include secrets or API keys.
Parameters:
feedback(required) — string — Detailed usage feedback from the agent. Long strings are accepted, but do not include secrets, API keys, or unrelated private data.options— object — Optional debug/context object. Prefer workflow_id or wf_id plus run_id when feedback concerns a workflow.
Hints: read-only no · destructive no · idempotent no · open-world no
Measured by MCPYP probing. Untested fields are omitted, never estimated. Listing is always free; MCPYP never proxies calls or stores user tokens.